﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Xml;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements.CWNotation
{
    /// <summary>
    /// Represents one note.
    /// </summary>
    public class Note : ModelElement, IComparable<Note>
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="Note"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Chord"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="presentedTone">
        /// <para>
        /// Type: <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The tone element presented by this note.
        /// </para>
        /// </param>
        /// <param name="currentEnvironment">
        /// <para>
        /// Type: <see cref="Environment"/>
        /// </para>
        /// <para>
        /// The staff environment for the note's position.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Note(XmlPackagePart packagePart, Chord owner, ToneElement presentedTone, Environment currentEnvironment)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");
            if (presentedTone == null)
                throw new ArgumentNullException("presentedTone");
            if (currentEnvironment == null)
                throw new ArgumentNullException("currentEnvironment");

            HeadPosition = NotePosition.Left;
            typeSizeAttribute.Value = double.NaN.ToString(PackageVocabulary.PackageCulture.NumberFormat);

            this.presentedTone = presentedTone;
            presentedToneAttribute.Value = presentedTone.ID.ToString();

            presentedTone.PropertyChanged += new PropertyChangedEventHandler(presentedTone_PropertyChanged);

            int staffPosition;
            AccidentalType accidentalType;
            currentEnvironment.GetNote(presentedTone.GetToneStruct(), out staffPosition, out accidentalType);
            staffPositionAttribute.Value = staffPosition.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            accidental = new Accidental(PackagePart, this, accidentalType);
            accidentalElement.AppendChild(accidental.Element);
            dotsPositionAttribute.Value = "0";
        }

        /// <summary>
        /// Creates a new <see cref="Note"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="XmlPackagePart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="Chord"/>
        /// </para>
        /// <para>
        /// The model element that directly owns this element.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal Note(XmlPackagePart packagePart, Chord owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            accidentalElement = element[PackageVocabulary.AccidentalElementName, PackageVocabulary.PackageNamespaceUri];
            if (accidentalElement.HasChildNodes)
                accidental = new Accidental(packagePart, this, (XmlElement)accidentalElement.FirstChild);

            dotsPositionAttribute = element.Attributes[PackageVocabulary.DotsPositionAttribute];
            headPositionAttribute = element.Attributes[PackageVocabulary.HeadPositionAttribute];
            presentedToneAttribute = element.Attributes[PackageVocabulary.PresentedToneAttribute];
            staffPositionAttribute = element.Attributes[PackageVocabulary.StaffPositionAttribute];
            typeSizeAttribute = element.Attributes[PackageVocabulary.TypeSizeAttribute];

            var tid = new Guid(presentedToneAttribute.Value);
            var chord = (ChordElement)FindOwner<Chord>().PresentedElement;
            presentedTone = chord.Tones.FirstOrDefault(t => t.ID == tid);

            if (presentedTone == null)
                throw new SerializationException("The tone ID " + tid + " was not found in the owner chord!", element.OuterXml);

            presentedTone.PropertyChanged += new PropertyChangedEventHandler(presentedTone_PropertyChanged);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.NoteElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            accidentalElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.AccidentalElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            dotsPositionAttribute = document.CreateAttribute(PackageVocabulary.DotsPositionAttribute);
            headPositionAttribute = document.CreateAttribute(PackageVocabulary.HeadPositionAttribute);
            presentedToneAttribute = document.CreateAttribute(PackageVocabulary.PresentedToneAttribute);
            staffPositionAttribute = document.CreateAttribute(PackageVocabulary.StaffPositionAttribute);
            typeSizeAttribute = document.CreateAttribute(PackageVocabulary.TypeSizeAttribute);

            element.Attributes.Append(dotsPositionAttribute);
            element.Attributes.Append(headPositionAttribute);
            element.Attributes.Append(presentedToneAttribute);
            element.Attributes.Append(staffPositionAttribute);
            element.Attributes.Append(typeSizeAttribute);

            element.AppendChild(accidentalElement);

            return element;
        }

        #endregion

        #region IComparable<Note> Members

        /// <inheritdoc/>
        public int CompareTo(Note other)
        {
            if (other == null)
                throw new ArgumentNullException("other");

            return StaffPosition - other.StaffPosition;
        }

        #endregion

        #region Event Handlers

        /// <summary>
        /// Occurs when the pitch of the <see cref="presentedTone"/> has changed.
        /// </summary>
        /// <remarks>
        /// <b>Operation:</b><br/>
        /// The handler retrieves the current staff environment from the owner staff and changes
        /// the staff position and accidental of the note accordingly to the presented tone.
        /// </remarks>
        /// <param name="sender">Object that has sent the event</param>
        /// <param name="e">Event data</param>
        private void presentedTone_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            var env = FindOwner<Staff>().GetEnvironmentFor(FindOwner<Chord>());
            int staffPosition;
            AccidentalType accidentalType;
            env.GetNote(presentedTone.GetToneStruct(), out staffPosition, out accidentalType);

            staffPositionAttribute.Value = staffPosition.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            RaisePropertyChanged("StaffPosition");
            accidental.Type = accidentalType;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Modifies the properties of the note to reflect the new staff environment.
        /// </summary>
        /// <param name="environment">
        /// <para>
        /// Type: <see cref="Environment"/>
        /// </para>
        /// <para>
        /// The new staff environment.
        /// </para>
        /// </param>
        public void ProcessEnvironmentChange(Environment environment)
        {
            int staffPosition;
            AccidentalType accidentalType;
            environment.GetNote(presentedTone.GetToneStruct(), out staffPosition, out accidentalType);

            staffPositionAttribute.Value = staffPosition.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            RaisePropertyChanged("StaffPosition");
            Accidental.Type = accidentalType;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the accidental of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="AccidentalType"/>
        /// </para>
        /// <para>
        /// The accidental of the note.
        /// </para>
        /// </value>
        public Accidental Accidental
        {
            get { return accidental; }
        }

        /// <summary>
        /// Gets or sets the staff position for the augmentation dots.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The staff position for the augmentation dots attached to this note
        /// relative to the staff position of this note.<br/>
        /// For example, a value of 1 means that the dots are placed one half space
        /// above the note.
        /// </para>
        /// </value>
        public int DotsPosition
        {
            get { return int.Parse(dotsPositionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                dotsPositionAttribute.Value = value.ToString();
                RaisePropertyChanged("DotsPosition");
            }
        }

        /// <summary>
        /// Gets the effective type size of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The effective type size (in centimeters) of the note.
        /// It is equal to <see cref="TypeSize"/> if <see cref="TypeSize"/>
        /// is <see cref="Double.NaN"/>. Otherwise, it is equal to <see cref="CoreSymbol.EffectiveTypeSize"/>
        /// of the owner <see cref="Chord"/>.
        /// </para>
        /// </value>
        public double EffectiveTypeSize
        {
            get
            {
                if (double.IsNaN(TypeSize))
                    return FindOwner<Chord>().EffectiveTypeSize;
                else
                    return TypeSize;
            }
        }

        /// <summary>
        /// Gets or sets the position of the note's head.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="NotePosition"/>
        /// </para>
        /// <para>
        /// The position of the note's head regarding to the note's stem.<br/>
        /// However, this value is expressed in terms of Common Western Music Notation.
        /// Since in the implementation model the note's stem is owned by the <see cref="Chord"/>
        /// that owns the <see cref="Note"/>. The exact meaning of this value is the position
        /// of the <see cref="Note"/> instance regarding to the stem of its owner <see cref="Chord"/>.
        /// </para>
        /// </value>
        public NotePosition HeadPosition
        {
            get { return (NotePosition)Enum.Parse(typeof(NotePosition), headPositionAttribute.Value); }
            set
            {
                headPositionAttribute.Value = value.ToString();

                RaisePropertyChanged("HeadPosition");
            }
        }

        /// <summary>
        /// Gets a reference to the tone element presented by this note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ToneElement"/>
        /// </para>
        /// <para>
        /// The presented tone element.
        /// </para>
        /// </value>
        public ToneElement PresentedTone
        {
            get { return presentedTone; }
        }

        /// <summary>
        /// Gets or sets the vertical position of the note on the staff.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Int32"/>
        /// </para>
        /// <para>
        /// The vertical position of the note on the staff.<br/>
        /// It is given as a staff line / space. The value 0 stands for the first (bottom) staff line.
        /// 1 stands for the first space above the first line, -1 stands for the first space bellow.
        /// 2 stands for the second line etc.
        /// </para>
        /// </value>
        public int StaffPosition
        {
            get { return int.Parse(staffPositionAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                var env = FindOwner<Staff>().GetEnvironmentFor(FindOwner<Chord>());
                presentedTone.SetTone(env.GetTone(value));
                FindOwner<Chord>().RaiseEnvironmentChanged();
            }
        }

        /// <summary>
        /// Gets or sets the type size of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The type size (in centimeters) of the note.<br/>
        /// The type size allows the note to have a different size than it would normally
        /// have on its owner <see cref="Staff"/>. It is given as a height of the staff
        /// that would correspond to this note size.<br/>
        /// To inherit the type size of the owner <see cref="Chord"/>, use the value <see cref="double.NaN"/> (default).
        /// </para>
        /// </value>
        /// <exception cref="ValidationException">
        /// The <paramref name="value"/> is equal to or less than 0.0.
        /// </exception>
        /// TODO: Write the validation rules
        public double TypeSize
        {
            get { return double.Parse(typeSizeAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            set
            {
                ValidateAndThrow("TypeSize", value);

                typeSizeAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("TypeSize");
            }
        }

        /// <summary>
        /// Gets the width of the note.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="Double"/>
        /// </para>
        /// <para>
        /// The width (in centimeters) of the note's head.
        /// </para>
        /// </value>
        public double Width
        {
            get
            {
                var staffSpaces = FindOwner<Staff>().Descriptor.Lines - 1;
                double baseSize;
                if (staffSpaces > 0)
                    baseSize = EffectiveTypeSize / (staffSpaces);
                else
                    baseSize = FindOwner<Staff>().SpaceSize;
                var duration = FindOwner<Chord>().BasicDuration;

                if (duration == DurationalSymbolElement.DoubleWhole)
                    return 2 * baseSize;
                else if (duration == DurationalSymbolElement.Whole)
                    return 1.5 * baseSize;
                else
                    return baseSize;
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Accidental"/> property.
        /// </summary>
        private Accidental accidental;
        /// <summary>
        /// Holds the XML element that holds the <see cref="Accidental"/> property value.
        /// </summary>
        private XmlElement accidentalElement;
        /// <summary>
        /// Holds the value of the <see cref="DotsPosition"/> property.
        /// </summary>
        private XmlAttribute dotsPositionAttribute;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="HeadPosition"/> property.
        /// </summary>
        private XmlAttribute headPositionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="PresentedTone"/> property.
        /// </summary>
        private ToneElement presentedTone;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="PresentedTone"/> property.
        /// </summary>
        private XmlAttribute presentedToneAttribute;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="StaffPosition"/> property.
        /// </summary>
        private XmlAttribute staffPositionAttribute;
        /// <summary>
        /// Holds the value of the <see cref="TypeSize"/> property.
        /// </summary>
        private XmlAttribute typeSizeAttribute;

        #endregion
    }

    /// <summary>
    /// Determines the position of the note regarding to the stem.
    /// </summary>
    /// <remarks>
    /// In the terms of Common Western Music Notation, this enum determines
    /// the position of the note's head regarding to the note's stem.
    /// However, since in the implementation model the stem is owned by
    /// a <see cref="Chord"/>, this enum gives the position of the <see cref="Note"/>
    /// regarding to the owner <see cref="Chord"/>'s stem.
    /// </remarks>
    public enum NotePosition
    {
        /// <summary>
        /// The note is positioned on the left side of the stem.
        /// </summary>
        Left,
        /// <summary>
        /// The note is positioned on the right side of the stem.
        /// </summary>
        Right
    }
}
